/**
* Copyright Alex Objelean
*/
package ro.isdc.wro.maven.plugin;
import static org.apache.commons.lang3.Validate.notNull;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.classworlds.ClassRealm;
import org.sonatype.plexus.build.incremental.BuildContext;
import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.extensions.manager.standalone.ExtensionsStandaloneManagerFactory;
import ro.isdc.wro.manager.WroManager;
import ro.isdc.wro.manager.factory.WroManagerFactory;
import ro.isdc.wro.manager.factory.standalone.StandaloneContext;
import ro.isdc.wro.manager.factory.standalone.StandaloneContextAware;
import ro.isdc.wro.maven.plugin.support.ExtraConfigFileAware;
import ro.isdc.wro.maven.plugin.support.ResourceChangeHandler;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.WroModelInspector;
import ro.isdc.wro.model.group.Group;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.util.concurrent.TaskExecutor;
import com.google.common.annotations.VisibleForTesting;
/**
* Defines most common properties used by wro4j build-time solution infrastructure.
*
* @author Alex Objelean
*/
public abstract class AbstractWro4jMojo
extends AbstractMojo {
/**
* File containing the groups definitions.
*
* @parameter default-value="${basedir}/src/main/webapp/WEB-INF/wro.xml" property="wroFile"
* @optional
*/
private File wroFile;
/**
* Allows clients to pass a build-time parameter to skip the plugin execution..
*
* @parameter default-value=false
* @optional
*/
private boolean skip;
/**
* The folder where web application context resides useful for locating resources relative to servletContext. It is
* possible to provide multiple context folders using a CSV. When multiple contextFolders are provided, the
* servletContext locator will try to search in next contextFolder when a resource could not be located. By default, a
* single context folder is configured.
*
* @parameter default-value="${basedir}/src/main/webapp/" property="contextFolder"
* @optional
*/
private String contextFolder;
/**
* @parameter default-value="true" property="minimize"
* @optional
*/
private boolean minimize;
/**
* @parameter property="ignoreMissingResources"
* @optional
*/
private String ignoreMissingResources;
/**
* Comma separated group names. This field is optional. If no value is provided, a file for each group will be
* created.
*
* @parameter property="targetGroups"
* @optional
*/
private String targetGroups;
/**
* @parameter default-value="${project}"
*/
private MavenProject mavenProject;
/**
* @parameter property="wroManagerFactory"
* @optional
*/
private String wroManagerFactory;
/**
* An instance of {@link StandaloneContextAware}.
*/
private WroManagerFactory managerFactory;
/**
* The path to configuration file.
*
* @parameter default-value="${basedir}/src/main/webapp/WEB-INF/wro.properties" property="extraConfigFile"
* @optional
*/
private File extraConfigFile;
/**
* Responsible for identifying the resources changed during incremental build.
* <p/>
* Read more about it <a href="http://wiki.eclipse.org/M2E_compatible_maven_plugins#BuildContext">here</a>
*
* @component
*/
private BuildContext buildContext;
/**
* This parameter is not meant to be used. The only purpose is to hold project build directory
*
* @parameter default-value="${project.build.directory}"
* @optional
*/
private File buildDirectory;
/**
* When this flag is enabled and there are more than one group to be processed, these will be processed in parallel,
* resulting in faster overall plugin execution time.
*
* @parameter default-value="false" property="parallelProcessing"
* @optional
*/
private boolean parallelProcessing;
/**
* Flag which allows to enable incremental build (experimental feature). It is false by default, but probably can be
* changed to true if no unexpected problems are detected..
*
* @parameter default-value="false" property="incrementalBuildEnabled"
* @optional
*/
private boolean incrementalBuildEnabled;
private TaskExecutor<Void> taskExecutor;
private ResourceChangeHandler resourceChangeHandler;
public final void execute()
throws MojoExecutionException {
if (skip) {
getLog().info("Skipping execution.");
} else {
validate();
if (buildDirectory == null) {
buildDirectory = new File(mavenProject.getModel().getBuild().getDirectory());
}
getLog().info(contextFolder);
getLog().info("Executing the mojo: ");
getLog().info("Wro4j Model path: " + wroFile.getPath());
getLog().info("targetGroups: " + getTargetGroups());
getLog().info("minimize: " + isMinimize());
getLog().info("ignoreMissingResources: " + isIgnoreMissingResources());
getLog().info("parallelProcessing: " + isParallelProcessing());
getLog().info("buildDirectory: " + buildDirectory);
getLog().debug("wroManagerFactory: " + wroManagerFactory);
getLog().debug("incrementalBuildEnabled: " + incrementalBuildEnabled);
getLog().debug("extraConfig: " + extraConfigFile);
extendPluginClasspath();
Context.set(Context.standaloneContext());
try {
onBeforeExecute();
doExecute();
} catch (final Exception e) {
final String message = "Exception occured while processing: " + e.toString() + ", class: "
+ e.getClass().getName() + ",caused by: " + (e.getCause() != null ? e.getCause().getClass().getName() : "");
getLog().error(message, e);
if (e instanceof WroRuntimeException) {
// Do not keep resources which cause the exception. This is helpful for linter processors.
final Resource resource = ((WroRuntimeException) e).getResource();
forgetResource(resource);
}
throw new MojoExecutionException(message, e);
} finally {
try {
onAfterExecute();
} catch (final Exception e) {
throw new MojoExecutionException("Exception in onAfterExecute", e);
}
}
}
}
/**
* Safely invoke {@link ResourceChangeHandler#forget(Resource)}. The safety is required because invoking
* {@link #getResourceChangeHandler()} can throw an exception during initialization.
*/
private void forgetResource(final Resource resource) {
if (resourceChangeHandler != null) {
resourceChangeHandler.forget(resource);
}
}
/**
* Creates a {@link StandaloneContext} by setting properties passed after mojo is initialized.
*/
private StandaloneContext createStandaloneContext() {
final StandaloneContext runContext = new StandaloneContext();
runContext.setContextFoldersAsCSV(getContextFoldersAsCSV());
runContext.setMinimize(isMinimize());
runContext.setWroFile(getWroFile());
runContext.setIgnoreMissingResourcesAsString(isIgnoreMissingResources());
return runContext;
}
/**
* Perform actual plugin processing.
*/
protected abstract void doExecute()
throws Exception;
/**
* This method will ensure that you have a right and initialized instance of {@link StandaloneContextAware}. When
* overriding this method, ensure that creating managerFactory performs injection during manager creation, otherwise
* the manager won't be initialized properly.
*
* @return {@link WroManagerFactory} implementation.
*/
protected WroManagerFactory getManagerFactory() {
if (managerFactory == null) {
WroManagerFactory localManagerFactory = null;
try {
localManagerFactory = newWroManagerFactory();
} catch (final MojoExecutionException e) {
throw WroRuntimeException.wrap(e);
}
// initialize before process.
if (localManagerFactory instanceof StandaloneContextAware) {
((StandaloneContextAware) localManagerFactory).initialize(createStandaloneContext());
}
managerFactory = decorateManagerFactory(localManagerFactory);
}
return managerFactory;
}
/**
* Allows the initialized manager factory to be decorated.
*/
protected WroManagerFactory decorateManagerFactory(final WroManagerFactory managerFactory) {
return managerFactory;
}
/**
* {@inheritDoc}
*/
protected WroManagerFactory newWroManagerFactory()
throws MojoExecutionException {
WroManagerFactory factory = null;
if (wroManagerFactory != null) {
factory = createCustomManagerFactory();
} else {
factory = new ExtensionsStandaloneManagerFactory();
}
getLog().info("wroManagerFactory class: " + factory.getClass().getName());
if (factory instanceof ExtraConfigFileAware) {
if (extraConfigFile == null) {
throw new MojoExecutionException("The " + factory.getClass() + " requires a valid extraConfigFile!");
}
getLog().debug("Using extraConfigFile: " + extraConfigFile.getAbsolutePath());
((ExtraConfigFileAware) factory).setExtraConfigFile(extraConfigFile);
}
return factory;
}
/**
* Creates an instance of Manager factory based on the value of the wroManagerFactory plugin parameter value.
*/
private WroManagerFactory createCustomManagerFactory()
throws MojoExecutionException {
WroManagerFactory managerFactory;
try {
final Class<?> wroManagerFactoryClass = Thread.currentThread().getContextClassLoader().loadClass(
wroManagerFactory.trim());
managerFactory = (WroManagerFactory) wroManagerFactoryClass.newInstance();
} catch (final Exception e) {
throw new MojoExecutionException("Invalid wroManagerFactoryClass, called: " + wroManagerFactory, e);
}
return managerFactory;
}
/**
* @return a list of groups which will be processed.
*/
protected final List<String> getTargetGroupsAsList()
throws Exception {
List<String> result = null;
if (isIncrementalCheckRequired()) {
result = getIncrementalGroupNames();
} else if (getTargetGroups() == null) {
result = getAllModelGroupNames();
} else {
result = Arrays.asList(getTargetGroups().split(","));
}
persistResourceFingerprints(result);
if (result.isEmpty()) {
getLog().info("Nothing to process (nothing configured or nothing changed since last build).");
} else {
getLog().info("The following groups will be processed: " + result);
}
return result;
}
/**
* Store digest for all resources contained inside the list of provided groups.
*/
private void persistResourceFingerprints(final List<String> groupNames) {
final WroModelInspector modelInspector = new WroModelInspector(getModel());
for (final String groupName : groupNames) {
final Group group = modelInspector.getGroupByName(groupName);
if (group != null) {
for (final Resource resource : group.getResources()) {
getResourceChangeHandler().remember(resource);
}
}
}
}
/**
* @return a list of groups changed by incremental builds.
*/
private List<String> getIncrementalGroupNames()
throws Exception {
final List<String> changedGroupNames = new ArrayList<String>();
for (final Group group : getModel().getGroups()) {
// skip processing non target groups
if (isTargetGroup(group)) {
for (final Resource resource : group.getResources()) {
getLog().debug("checking delta for resource: " + resource);
if (getResourceChangeHandler().isResourceChanged(resource)) {
getLog().debug("detected change for resource: " + resource + " and group: " + group.getName());
changedGroupNames.add(group.getName());
// no need to check rest of resources from this group
break;
}
}
}
}
return changedGroupNames;
}
/**
* Check if the provided group is a target group.
*/
private boolean isTargetGroup(final Group group) {
notNull(group);
final String targetGroups = getTargetGroups();
// null, means all groups are target groups
return targetGroups == null || targetGroups.contains(group.getName());
}
/**
* Checks if all required fields are configured.
*/
protected void validate()
throws MojoExecutionException {
if (wroFile == null) {
throw new MojoExecutionException("contextFolder was not set!");
}
if (contextFolder == null) {
throw new MojoExecutionException("no contextFolder was set!");
}
}
/**
* Update the classpath.
*/
protected final void extendPluginClasspath()
throws MojoExecutionException {
// this code is inspired from http://teleal.org/weblog/Extending%20the%20Maven%20plugin%20classpath.html
final List<String> classpathElements = new ArrayList<String>();
try {
classpathElements.addAll(mavenProject.getRuntimeClasspathElements());
} catch (final DependencyResolutionRequiredException e) {
throw new MojoExecutionException("Could not get compile classpath elements", e);
}
final ClassLoader classLoader = createClassLoader(classpathElements);
Thread.currentThread().setContextClassLoader(classLoader);
}
/**
* @return {@link ClassRealm} based on project dependencies.
*/
private ClassLoader createClassLoader(final List<String> classpathElements) {
getLog().debug("Classpath elements:");
final List<URL> urls = new ArrayList<URL>();
try {
for (final String element : classpathElements) {
final File elementFile = new File(element);
getLog().debug("Adding element to plugin classpath: " + elementFile.getPath());
urls.add(elementFile.toURI().toURL());
}
} catch (final Exception e) {
getLog().error("Error retreiving URL for artifact", e);
throw new RuntimeException(e);
}
return new URLClassLoader(urls.toArray(new URL[] {}), Thread.currentThread().getContextClassLoader());
}
/**
* @return The {@link TaskExecutor} responsible for running multiple tasks in parallel.
*/
protected final TaskExecutor<Void> getTaskExecutor() {
if (taskExecutor == null) {
taskExecutor = new TaskExecutor<Void>() {
@Override
protected void onException(final Exception e) {
// propagate exception
throw WroRuntimeException.wrap(e);
}
};
}
return taskExecutor;
}
/**
* @return true if the only incremental changed group should be used as target groups for next processing.
*/
protected boolean isIncrementalCheckRequired() {
return isIncrementalBuild();
}
/**
* Invoked before execution is performed.
*/
protected void onBeforeExecute() {
}
/**
* Invoked right after execution completion. This method is invoked also if the execution failed with an exception.
*/
protected void onAfterExecute() {
resourceChangeHandler.persist();
}
/**
* @return true if the build was triggered by an incremental change.
*/
protected final boolean isIncrementalBuild() {
return getResourceChangeHandler().isIncrementalBuild();
}
private List<String> getAllModelGroupNames() {
return new WroModelInspector(getModel()).getGroupNames();
}
private WroModel getModel() {
return getWroManager().getModelFactory().create();
}
private WroManager getWroManager() {
try {
return getManagerFactory().create();
} catch (final Exception e) {
throw WroRuntimeException.wrap(e);
}
}
private ResourceChangeHandler getResourceChangeHandler() {
if (resourceChangeHandler == null) {
resourceChangeHandler = ResourceChangeHandler.create(getManagerFactory(), getLog()).setBuildContext(buildContext).setBuildDirectory(
buildDirectory).setIncrementalBuildEnabled(incrementalBuildEnabled);
}
return resourceChangeHandler;
}
@VisibleForTesting
void setTaskExecutor(final TaskExecutor<Void> taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* @param contextFolder
* the servletContextFolder to set
* @VisibleForTesting
*/
String getContextFoldersAsCSV() {
return contextFolder;
}
/**
* @param contextFolders
* a CSV representing contextFolders to use.
* @VisibleForTesting
*/
void setContextFolder(final String contextFolder) {
this.contextFolder = contextFolder;
}
/**
* @param wroFile
* the wroFile to set
* @VisibleForTesting
*/
void setWroFile(final File wroFile) {
this.wroFile = wroFile;
}
/**
* @return the wroFile
* @VisibleForTesting
*/
File getWroFile() {
return this.wroFile;
}
/**
* @param minimize
* flag for minimization.
* @VisibleForTesting
*/
void setMinimize(final boolean minimize) {
this.minimize = minimize;
}
/**
* @param ignoreMissingResourcesAsString
* the ignoreMissingResources to set
* @VisibleForTesting
*/
void setIgnoreMissingResources(final String ignoreMissingResourcesAsString) {
this.ignoreMissingResources = ignoreMissingResourcesAsString;
}
void setIgnoreMissingResources(final boolean ignoreMissingResources) {
setIgnoreMissingResources(Boolean.toString(ignoreMissingResources));
}
/**
* @VisibleForTesting
*/
protected final boolean isParallelProcessing() {
return parallelProcessing;
}
/**
* @VisibleForTesting
*/
final void setParallelProcessing(final boolean parallelProcessing) {
this.parallelProcessing = parallelProcessing;
}
/**
* @VisibleForTesting
*/
void setIncrementalBuildEnabled(final boolean incrementalBuildEnabled) {
this.incrementalBuildEnabled = incrementalBuildEnabled;
}
/**
* @return the minimize
* @VisibleForTesting
*/
boolean isMinimize() {
return this.minimize;
}
/**
* @return the ignoreMissingResources
* @VisibleForTesting
*/
String isIgnoreMissingResources() {
return this.ignoreMissingResources;
}
/**
* Used for testing.
*
* @param mavenProject
* the mavenProject to set
*/
void setMavenProject(final MavenProject mavenProject) {
this.mavenProject = mavenProject;
}
/**
* @return the targetGroups
* @VisibleForTesting
*/
String getTargetGroups() {
return this.targetGroups;
}
/**
* @param versionEncoder
* (targetGroups) comma separated group names.
* @VisibleForTesting
*/
void setTargetGroups(final String targetGroups) {
this.targetGroups = targetGroups;
}
/**
* @param wroManagerFactory
* fully qualified name of the {@link WroManagerFactory} class.
* @VisibleForTesting
*/
void setWroManagerFactory(final String wroManagerFactory) {
this.wroManagerFactory = wroManagerFactory;
}
/**
* @param extraConfigFile
* the extraConfigFile to set
* @VisibleForTesting
*/
void setExtraConfigFile(final File extraConfigFile) {
this.extraConfigFile = extraConfigFile;
}
/**
* @VisibleForTesting
*/
void setBuildContext(final BuildContext buildContext) {
this.buildContext = buildContext;
}
/**
* @VisibleForTesting
*/
void setSkip(final boolean skip) {
this.skip = skip;
}
/**
* Removes any persisted data creating during the build.
*
* @VisibleForTesting
*/
void clean() {
try {
getResourceChangeHandler().destroy();
} catch (final Exception e) {
// do not propagate the error during cleanup
getLog().error("Failed to destroy resourceChangeHandler", e);
}
}
}